/*
 * FTNANDC024 driver developed by Mychaela Falconia at CarrierComm, Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <linux/io.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/interrupt.h>
#include <linux/ioport.h>
#include <linux/mtd/mtd.h>
#include <linux/wait.h>
#include <linux/errno.h>

#include "ftnandc024_priv.h"
#include "ftnandc024_regs.h"

static int read_page_into_sram(struct mtd_info *mtd, unsigned row_addr,
				bool oob)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	int rc;
	uint32_t command, eccstat;

	if (priv->page_in_buffer == row_addr && !oob)
		return 0;

	/* clear controller state before issuing command */
	writel(0xFFFFFFFF, priv->io_base + ECC_INTR_STATUS);
	writel(0, priv->io_base + BMC_USER_ADJUST(0));

	/* issue command to read the requested page */
	writel(row_addr, priv->io_base + CMDQUEUE1(0));
	writel(CMD_COUNT(priv->sectors_per_page), priv->io_base + CMDQUEUE3(0));
	if (priv->smallpage_cmds)
		command = CMD_INDEX(SMALL_FIXFLOW_PAGEREAD) |
				CMD_SPARE_NUM(mtd->oobsize);
	else if (oob)
		command = CMD_INDEX(LARGE_FIXFLOW_PAGEREAD_OOB) |
				CMD_SPARE_NUM(mtd->oobsize);
	else
		command = CMD_INDEX(LARGE_FIXFLOW_PAGEREAD);
	rc = ftnandc024_execute_command(priv, command | CMD_USER_MODE);
	if (unlikely(rc)) {
		priv->page_in_buffer = -1;
		return rc;
	}

	/* done, check ECC status */
	eccstat = readl(priv->io_base + ECC_INTR_STATUS);
	if (eccstat & ECC_ERR_FAIL(0)) {
		mtd->ecc_stats.failed++;
		priv->page_in_buffer = -1;
	} else {
		priv->page_in_buffer = row_addr;
		if (eccstat & ECC_ERR_THRES_HIT(0))
			mtd->ecc_stats.corrected++;
	}

	/* check OOB ECC status */
	if (unlikely(oob)) {
		if (eccstat & ECC_ERR_SP_FAIL(0))
			mtd->ecc_stats.failed++;
		else if (eccstat & ECC_ERR_SP_THRES_HIT(0))
			mtd->ecc_stats.corrected++;
	}

	return 0;
}

static int read_under_mutex(struct mtd_info *mtd, loff_t from, size_t len,
			    size_t *retlen, u_char *buf)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	size_t startlen = len;
	unsigned chunk;
	int rc = 0;
	struct mtd_ecc_stats starting_ecc_stats = mtd->ecc_stats;

	if (unlikely(priv->reset_needed)) {
		rc = ftnandc024_reset_nand_chip(priv);
		if (rc) {
			*retlen = 0;
			return rc;
		}
	}

	/* handle non-page-aligned start first */
	if (from & mtd->writesize_mask) {
		unsigned offset = from & mtd->writesize_mask;

		rc = read_page_into_sram(mtd, from >> mtd->writesize_shift,
					 false);
		if (unlikely(rc)) {
			*retlen = 0;
			return rc;
		}
		chunk = mtd->writesize - offset;
		if (chunk > len)
			chunk = len;
		memcpy(buf, priv->data_sram + offset, chunk);
		buf += chunk;
		from += chunk;
		len -= chunk;
	}

	/* read full pages and/or trailing partial page */
	while (len) {
		rc = read_page_into_sram(mtd, from >> mtd->writesize_shift,
					 false);
		if (unlikely(rc))
			break;
		chunk = mtd->writesize;
		if (chunk > len)
			chunk = len;
		memcpy(buf, priv->data_sram, chunk);
		buf += chunk;
		from += chunk;
		len -= chunk;
	}
	*retlen = startlen - len;
	if (rc)
		return rc;

	/* return ECC status */
	if (mtd->ecc_stats.failed - starting_ecc_stats.failed)
		return -EBADMSG;
	else if (mtd->ecc_stats.corrected - starting_ecc_stats.corrected)
		return -EUCLEAN;
	else
		return 0;
}

int ftnandc024_mtd_read_method(struct mtd_info *mtd, loff_t from, size_t len,
				size_t *retlen, u_char *buf)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	int ret;

	/* Do not allow reads past end of device */
	if ((from + len) > mtd->size)
		return -EINVAL;
	if (!len)
		return 0;

	mutex_lock(&priv->mutex);
	ret = read_under_mutex(mtd, from, len, retlen, buf);
	mutex_unlock(&priv->mutex);
	return ret;
}

static int readops_data_and_oob(struct mtd_info *mtd, loff_t from,
				struct mtd_oob_ops *ops)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	size_t readlen = ops->len;
	size_t oobreadlen = ops->ooblen;
	uint8_t *buf = ops->datbuf;
	uint8_t *oob = ops->oobbuf;
	unsigned chunk;
	int rc = 0;
	struct mtd_ecc_stats starting_ecc_stats = mtd->ecc_stats;

	if (unlikely(priv->reset_needed)) {
		rc = ftnandc024_reset_nand_chip(priv);
		if (rc)
			return rc;
	}

	/* handle non-page-aligned start first */
	if (from & mtd->writesize_mask) {
		unsigned offset = from & mtd->writesize_mask;

		rc = read_page_into_sram(mtd, from >> mtd->writesize_shift,
					 true);
		if (unlikely(rc))
			return rc;
		chunk = mtd->writesize - offset;
		if (chunk > readlen)
			chunk = readlen;
		memcpy(buf, priv->data_sram + offset, chunk);
		buf += chunk;
		from += chunk;
		readlen -= chunk;
		/* handle OOB */
		if (oob && oobreadlen) {
			chunk = mtd->oobsize;
			if (chunk > oobreadlen)
				chunk = oobreadlen;
			memcpy(oob, priv->io_base + SPARE_SRAM + ops->ooboffs,
				chunk);
			oob += chunk;
			oobreadlen -= chunk;
		}
	}

	/* read full pages and/or trailing partial page */
	while (readlen) {
		rc = read_page_into_sram(mtd, from >> mtd->writesize_shift,
					 true);
		if (unlikely(rc))
			break;
		chunk = mtd->writesize;
		if (chunk > readlen)
			chunk = readlen;
		memcpy(buf, priv->data_sram, chunk);
		buf += chunk;
		from += chunk;
		readlen -= chunk;
		/* handle OOB */
		if (oob && oobreadlen) {
			chunk = mtd->oobsize;
			if (chunk > oobreadlen)
				chunk = oobreadlen;
			memcpy(oob, priv->io_base + SPARE_SRAM + ops->ooboffs,
				chunk);
			oob += chunk;
			oobreadlen -= chunk;
		}
	}
	ops->retlen = ops->len - readlen;
	if (oob)
		ops->oobretlen = ops->ooblen - oobreadlen;

	if (rc)
		return rc;

	/* return ECC status */
	if (mtd->ecc_stats.failed - starting_ecc_stats.failed)
		return -EBADMSG;
	else if (mtd->ecc_stats.corrected - starting_ecc_stats.corrected)
		return -EUCLEAN;
	else
		return 0;
}

static int read_onepage_oob(struct mtd_info *mtd, unsigned row_addr)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	int rc;
	uint32_t command, eccstat;

	/* clear controller state before issuing command */
	writel(0xFFFFFFFF, priv->io_base + ECC_INTR_STATUS);

	/* issue command to read the OOB area of the requested page */
	writel(row_addr, priv->io_base + CMDQUEUE1(0));
	writel(CMD_COUNT(1), priv->io_base + CMDQUEUE3(0));
	if (priv->smallpage_cmds)
		command = CMD_INDEX(SMALL_FIXFLOW_READOOB);
	else
		command = CMD_INDEX(LARGE_FIXFLOW_READOOB);
	command |= CMD_SPARE_NUM(mtd->oobsize);
	rc = ftnandc024_execute_command(priv, command);
	if (unlikely(rc))
		return rc;

	/* done, check ECC status */
	eccstat = readl(priv->io_base + ECC_INTR_STATUS);
	if (eccstat & ECC_ERR_SP_FAIL(0))
		mtd->ecc_stats.failed++;
	else if (eccstat & ECC_ERR_SP_THRES_HIT(0))
		mtd->ecc_stats.corrected++;

	return 0;
}

static int readops_oob_only(struct mtd_info *mtd, loff_t from,
			    struct mtd_oob_ops *ops)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	unsigned row_addr = from >> mtd->writesize_shift;
	size_t readlen = ops->ooblen;
	uint8_t *oob = ops->oobbuf;
	unsigned chunk;
	int rc = 0;
	struct mtd_ecc_stats starting_ecc_stats = mtd->ecc_stats;

	if (unlikely(priv->reset_needed)) {
		rc = ftnandc024_reset_nand_chip(priv);
		if (rc)
			return rc;
	}

	while (readlen) {
		rc = read_onepage_oob(mtd, row_addr);
		if (unlikely(rc))
			break;
		chunk = mtd->oobsize;
		if (chunk > readlen)
			chunk = readlen;
		memcpy(oob, priv->io_base + SPARE_SRAM + ops->ooboffs, chunk);
		oob += chunk;
		readlen -= chunk;
		row_addr++;
	}

	ops->oobretlen = ops->ooblen - readlen;
	if (rc)
		return rc;

	/* return ECC status */
	if (mtd->ecc_stats.failed - starting_ecc_stats.failed)
		return -EBADMSG;
	else if (mtd->ecc_stats.corrected - starting_ecc_stats.corrected)
		return -EUCLEAN;
	else
		return 0;
}

int ftnandc024_mtd_readoob_method(struct mtd_info *mtd, loff_t from,
				  struct mtd_oob_ops *ops)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	int rc;

	ops->retlen = 0;
	ops->oobretlen = 0;

	/* sanity checks */
	if (ops->mode != MTD_OPS_PLACE_OOB && ops->mode != MTD_OPS_AUTO_OOB)
		return -EOPNOTSUPP;
	/* don't allow reads past the end of the device */
	if (unlikely(from >= mtd->size))
		return -EINVAL;
	if (ops->datbuf && ops->len > (mtd->size - from))
		return -EINVAL;
	if (unlikely(ops->ooboffs >= mtd->oobsize))
		return -EINVAL;
	if (ops->ooboffs && ops->ooblen > (mtd->oobsize - ops->ooboffs))
		return -EINVAL;
	if (unlikely(ops->ooblen > ((mtd->size >> mtd->writesize_shift) -
				    (from >> mtd->writesize_shift)) *
				   mtd->oobsize))
		return -EINVAL;

	mutex_lock(&priv->mutex);
	if (ops->datbuf)
		rc = readops_data_and_oob(mtd, from, ops);
	else
		rc = readops_oob_only(mtd, from, ops);
	mutex_unlock(&priv->mutex);
	return rc;
}
